package com.drinks.yiniang.common.glide;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

import com.bumptech.glide.load.Key;

import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.sql.DataSource;

/**
 * An exception with zero or more causes indicating why a load in Glide failed.
 */

public class GlideException extends Exception {
    private static final StackTraceElement[] EMPTY_ELEMENTS = new StackTraceElement[0];

    private final List<Exception> causes;
    private Key key;
    private DataSource dataSource;
    private Class<?> dataClass;

    public GlideException(String message) {
        this(message, Collections.<Exception>emptyList());
    }

    public GlideException(String detailMessage, Exception cause) {
        this(detailMessage, Collections.singletonList(cause));
    }

    public GlideException(String detailMessage, List<Exception> causes) {
        super(detailMessage);
        setStackTrace(EMPTY_ELEMENTS);
        this.causes = causes;
    }

    void setLoggingDetails(Key key, DataSource dataSource) {
        setLoggingDetails(key, dataSource, null);
    }

    void setLoggingDetails(Key key, DataSource dataSource, Class<?> dataClass) {
        this.key = key;
        this.dataSource = dataSource;
        this.dataClass = dataClass;
    }

    @Override
    public Throwable fillInStackTrace() {
        // Avoid an expensive allocation by doing nothing here. Causes should contain all relevant
        // stack traces.
        return this;
    }

    /**
     * Returns a list of causes that are immediate children of this exception.
     *
     * <p>Causes may or may not be {@link GlideException GlideExceptions}. Causes may also not be root
     * causes, and in turn my have been caused by other failures.</p>
     *
     * @see #getRootCauses()
     */
    public List<Exception> getCauses() {
        return causes;
    }

    /**
     * Returns the list of root causes that are the leaf nodes of all children of this exception.
     *
     * <p>Use this method to do things like look for http exceptions that indicate the load may have
     * failed due to an error that can be retried. Keep in mind that because Glide may attempt to load
     * a given model using multiple different pathways, there may be multiple related or unrelated
     * reasons for a load to fail.
     */
    public List<Exception> getRootCauses() {
        List<Exception> rootCauses = new ArrayList<>();
        addRootCauses(this, rootCauses);
        return rootCauses;
    }

    /**
     * Logs all root causes using the given tag.
     *
     * <p>Each root cause is logged separately to avoid throttling. {@link #printStackTrace()} will
     * provide a more succinct overview of why the exception occurred, although it does not include
     * complete stack traces.
     */
    public void logRootCauses(String tag) {
        Log.e(tag, getClass() + ": " + getMessage());
        List<Exception> causes = getRootCauses();
        for (int i = 0, size = causes.size(); i < size; i++) {
            Log.i(tag, "Root cause (" + (i + 1) + " of " + size + ")", causes.get(i));
        }
    }

    private void addRootCauses(Exception exception, List<Exception> rootCauses) {
        if (exception instanceof GlideException) {
            GlideException glideException = (GlideException) exception;
            for (Exception e : glideException.getCauses()) {
                addRootCauses(e, rootCauses);
            }
        } else {
            rootCauses.add(exception);
        }
    }

    @Override
    public void printStackTrace() {
        printStackTrace(System.err);
    }

    @Override
    public void printStackTrace(PrintStream err) {
        printStackTrace((Appendable) err);
    }

    @Override
    public void printStackTrace(PrintWriter err) {
        printStackTrace((Appendable) err);
    }

    private void printStackTrace(Appendable appendable) {
        appendExceptionMessage(this, appendable);
        appendCauses(getCauses(), new IndentedAppendable(appendable));
    }

    @Override
    public String getMessage() {
        return super.getMessage()
                + (dataClass != null ? ", " + dataClass : "")
                + (dataSource != null ? ", " + dataSource : "")
                + (key != null ? ", " + key : "");
    }

    // Appendable throws, PrintWriter, PrintStream, and IndentedAppendable do not, so this should
    // never happen.
    @SuppressWarnings("PMD.PreserveStackTrace")
    private static void appendExceptionMessage(Exception e, Appendable appendable) {
        try {
            appendable.append(e.getClass().toString()).append(": ").append(e.getMessage()).append('\n');
        } catch (IOException e1) {
            throw new RuntimeException(e);
        }
    }

    // Appendable throws, PrintWriter, PrintStream, and IndentedAppendable do not, so this should
    // never happen.
    @SuppressWarnings("PMD.PreserveStackTrace")
    private static void appendCauses(List<Exception> causes, Appendable appendable) {
        try {
            appendCausesWrapped(causes, appendable);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
    private static void appendCausesWrapped(List<Exception> causes, Appendable appendable)
            throws IOException {
        int size = causes.size();
        for (int i = 0; i < size; i++) {
            appendable.append("Cause (")
                    .append(String.valueOf(i + 1))
                    .append(" of ")
                    .append(String.valueOf(size))
                    .append("): ");

            Exception cause = causes.get(i);
            if (cause instanceof GlideException) {
                GlideException glideCause = (GlideException) cause;
                glideCause.printStackTrace(appendable);
            } else {
                appendExceptionMessage(cause, appendable);
            }
        }
    }

    private static final class IndentedAppendable implements Appendable {
        private static final String EMPTY_SEQUENCE = "";
        private static final String INDENT = "  ";
        private final Appendable appendable;
        private boolean printedNewLine = true;

        IndentedAppendable(Appendable appendable) {
            this.appendable = appendable;
        }

        @Override
        public Appendable append(char c) throws IOException {
            if (printedNewLine) {
                printedNewLine = false;
                appendable.append(INDENT);
            }
            printedNewLine = c == '\n';
            appendable.append(c);
            return this;
        }

        @Override
        public Appendable append(@Nullable CharSequence charSequence) throws IOException {
            charSequence = safeSequence(charSequence);
            return append(charSequence, 0, charSequence.length());
        }

        @Override
        public Appendable append(@Nullable CharSequence charSequence, int start, int end)
                throws IOException {
            charSequence = safeSequence(charSequence);
            if (printedNewLine) {
                printedNewLine = false;
                appendable.append(INDENT);
            }
            printedNewLine = charSequence.length() > 0 && charSequence.charAt(end - 1) == '\n';
            appendable.append(charSequence, start, end);
            return this;
        }

        @NonNull
        private CharSequence safeSequence(@Nullable CharSequence sequence) {
            if (sequence == null) {
                return EMPTY_SEQUENCE;
            } else {
                return sequence;
            }
        }
    }
}
